// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.XPath;
using ILLink.Shared;
using ILLink.Shared.TrimAnalysis;
using ILLink.Shared.TypeSystemProxy;
using Mono.Cecil;

namespace Mono.Linker.Steps
{
    public class LinkAttributesParser : ProcessLinkerXmlBase
    {
        AttributeInfo? _attributeInfo;

        public LinkAttributesParser(LinkContext context, Stream documentStream, string xmlDocumentLocation)
            : base(context, documentStream, xmlDocumentLocation)
        {
        }

        public LinkAttributesParser(LinkContext context, Stream documentStream, EmbeddedResource resource, AssemblyDefinition resourceAssembly, string xmlDocumentLocation = "<unspecified>")
            : base(context, documentStream, resource, resourceAssembly, xmlDocumentLocation)
        {
        }

        public void Parse(AttributeInfo xmlInfo)
        {
            _attributeInfo = xmlInfo;
            bool stripLinkAttributes = _context.IsOptimizationEnabled(CodeOptimizations.RemoveLinkAttributes, _resource?.Assembly);
            ProcessXml(stripLinkAttributes, _context.IgnoreLinkAttributes);
        }

        static bool IsRemoveAttributeInstances(string attributeName) => attributeName == "RemoveAttributeInstances" || attributeName == "RemoveAttributeInstancesAttribute";

        (CustomAttribute[]? customAttributes, MessageOrigin[]? origins) ProcessAttributes(XPathNavigator nav, ICustomAttributeProvider provider)
        {
            ArrayBuilder<CustomAttribute> customAttributesBuilder = default;
            ArrayBuilder<MessageOrigin> originsBuilder = default;
            foreach (XPathNavigator attributeNav in nav.SelectChildren("attribute", string.Empty))
            {
                if (!ShouldProcessElement(attributeNav))
                    continue;

                TypeDefinition? attributeType;
                string internalAttribute = GetAttribute(attributeNav, "internal");
                if (!string.IsNullOrEmpty(internalAttribute))
                {
                    if (!IsRemoveAttributeInstances(internalAttribute))
                    {
                        LogWarning(attributeNav, DiagnosticId.UnrecognizedInternalAttribute, internalAttribute);
                        continue;
                    }
                    if (provider is not TypeDefinition)
                    {
                        LogWarning(attributeNav, DiagnosticId.XmlRemoveAttributeInstancesCanOnlyBeUsedOnType, nameof(RemoveAttributeInstancesAttribute));
                        continue;
                    }

                    attributeType = GenerateRemoveAttributeInstancesAttribute();
                    if (attributeType == null)
                        continue;
                }
                else
                {
                    string attributeFullName = GetFullName(attributeNav);
                    if (string.IsNullOrEmpty(attributeFullName))
                    {
                        LogWarning(attributeNav, DiagnosticId.XmlElementDoesNotContainRequiredAttributeFullname);
                        continue;
                    }

                    if (!GetAttributeType(attributeNav, attributeFullName, out attributeType))
                        continue;
                }

                CustomAttribute? customAttribute = CreateCustomAttribute(attributeNav, attributeType, provider);
                if (customAttribute != null)
                {
                    _context.LogMessage($"Assigning external custom attribute '{FormatCustomAttribute(customAttribute)}' instance to '{provider}'.");
                    customAttributesBuilder.Add(customAttribute);
                    originsBuilder.Add(GetMessageOriginForPosition(attributeNav));
                }
            }

            return (customAttributesBuilder.ToArray(), originsBuilder.ToArray());

            static string FormatCustomAttribute(CustomAttribute ca)
            {
                StringBuilder sb = new StringBuilder();
                sb.Append(ca.Constructor.GetDisplayName());
                sb.Append(" { args: ");
                for (int i = 0; i < ca.ConstructorArguments.Count; ++i)
                {
                    if (i > 0)
                        sb.Append(", ");

                    var caa = ca.ConstructorArguments[i];
                    sb.Append($"{caa.Type.GetDisplayName()} {caa.Value}");
                }
                sb.Append(" }");

                return sb.ToString();
            }
        }
        TypeDefinition? GenerateRemoveAttributeInstancesAttribute()
        {
            TypeDefinition? td = null;

            if (_context.MarkedKnownMembers.RemoveAttributeInstancesAttributeDefinition is TypeDefinition knownTypeDef)
            {
                return knownTypeDef;
            }

            var voidType = BCL.FindPredefinedType(WellKnownType.System_Void, _context);
            if (voidType == null)
                return null;

            var attributeType = BCL.FindPredefinedType(WellKnownType.System_Attribute, _context);
            if (attributeType == null)
                return null;

            var objectType = BCL.FindPredefinedType(WellKnownType.System_Object, _context);
            if (objectType == null)
                return null;
            var objectArrayType = new ArrayType(objectType);
            if (objectArrayType == null)
                return null;

            //
            // Generates metadata information for internal type
            //
            // public sealed class RemoveAttributeInstancesAttribute : Attribute
            // {
            //  public RemoveAttributeInstancesAttribute() {}
            //  public RemoveAttributeInstancesAttribute(object values) {} // For legacy uses
            //  public RemoveAttributeInstancesAttribute(params object[] values) {}
            // }
            //
            const MethodAttributes ctorAttributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName | MethodAttributes.Final;

            td = new TypeDefinition("", nameof(RemoveAttributeInstancesAttribute), TypeAttributes.Public);
            td.BaseType = attributeType;

            var ctor = new MethodDefinition(".ctor", ctorAttributes, voidType);
            td.Methods.Add(ctor);
            var ctor1 = new MethodDefinition(".ctor", ctorAttributes, voidType);
            var param = new ParameterDefinition(objectType);
            td.Methods.Add(ctor1);

            var ctorN = new MethodDefinition(".ctor", ctorAttributes, voidType);
            var paramN = new ParameterDefinition(objectArrayType);
#pragma warning disable RS0030 // MethodReference.Parameters is banned. It's necessary to build the method definition here, though.
            ctorN.Parameters.Add(paramN);
#pragma warning restore RS0030
            td.Methods.Add(ctorN);

            return _context.MarkedKnownMembers.RemoveAttributeInstancesAttributeDefinition = td;
        }

        CustomAttribute? CreateCustomAttribute(XPathNavigator nav, TypeDefinition attributeType, ICustomAttributeProvider provider)
        {
            CustomAttributeArgument[] arguments = ReadCustomAttributeArguments(nav, provider);

            MethodDefinition? constructor = FindBestMatchingConstructor(attributeType, arguments);
            if (constructor == null)
            {
                LogWarning(nav, DiagnosticId.XmlCouldNotFindMatchingConstructorForCustomAttribute, attributeType.GetDisplayName());
                return null;
            }

            CustomAttribute customAttribute = new CustomAttribute(constructor);
            foreach (var argument in arguments)
                customAttribute.ConstructorArguments.Add(argument);

            ReadCustomAttributeProperties(nav, attributeType, customAttribute);

            return customAttribute;
        }

        MethodDefinition? FindBestMatchingConstructor(TypeDefinition attributeType, CustomAttributeArgument[] args)
        {
            var methods = attributeType.Methods;
            for (int i = 0; i < attributeType.Methods.Count; ++i)
            {
                var method = methods[i];
                if (!method.IsInstanceConstructor())
                    continue;

                if (args.Length != method.GetMetadataParametersCount())
                    continue;

                bool match = true;
                foreach (var p in method.GetMetadataParameters())
                {
                    //
                    // No candidates betterness, only exact matches are supported
                    //
                    var parameterType = _context.TryResolve(p.ParameterType);
                    if (parameterType == null || parameterType != _context.TryResolve(args[p.MetadataIndex].Type))
                        match = false;
                }

                if (match)
                    return method;
            }

            return null;
        }

        void ReadCustomAttributeProperties(XPathNavigator nav, TypeDefinition attributeType, CustomAttribute customAttribute)
        {
            foreach (XPathNavigator propertyNav in nav.SelectChildren("property", string.Empty))
            {
                string propertyName = GetName(propertyNav);
                if (string.IsNullOrEmpty(propertyName))
                {
                    LogWarning(propertyNav, DiagnosticId.XmlPropertyDoesNotContainAttributeName);
                    continue;
                }

                PropertyDefinition? property = attributeType.Properties.Where(prop => prop.Name == propertyName).FirstOrDefault();
                if (property == null)
                {
                    LogWarning(propertyNav, DiagnosticId.XmlCouldNotFindProperty, propertyName);
                    continue;
                }

                var caa = ReadCustomAttributeArgument(propertyNav, property);
                if (caa is null)
                    continue;

                customAttribute.Properties.Add(new CustomAttributeNamedArgument(property.Name, caa.Value));
            }
        }

        CustomAttributeArgument[] ReadCustomAttributeArguments(XPathNavigator nav, ICustomAttributeProvider provider)
        {
            ArrayBuilder<CustomAttributeArgument> args = default;

            foreach (XPathNavigator argumentNav in nav.SelectChildren("argument", string.Empty))
            {
                CustomAttributeArgument? caa = ReadCustomAttributeArgument(argumentNav, provider);
                if (caa is not null)
                    args.Add(caa.Value);
            }

            return args.ToArray() ?? Array.Empty<CustomAttributeArgument>();
        }

        CustomAttributeArgument? ReadCustomAttributeArgument(XPathNavigator nav, ICustomAttributeProvider provider)
        {
            TypeReference? typeref = ResolveArgumentType(nav, provider);
            if (typeref is null)
                return null;

            string svalue = nav.Value;

            //
            // Builds CustomAttributeArgument in the same way as it would be
            // represented in the metadata if encoded there. This simplifies
            // any custom attributes handling in ILLink by using same attributes
            // value extraction or mathing logic.
            //
            switch (typeref.MetadataType)
            {
                case MetadataType.Object:
                    var argumentIterator = nav.SelectChildren("argument", string.Empty);
                    if (argumentIterator?.MoveNext() != true)
                    {
                        _context.LogError(null, DiagnosticId.CustomAttributeArgumentForTypeRequiresNestedNode, "System.Object", "argument");
                        return null;
                    }

                    var typedef = _context.TryResolve(typeref);
                    if (typedef == null)
                        return null;

                    var boxedValue = ReadCustomAttributeArgument(argumentIterator.Current!, typedef);
                    if (boxedValue is null)
                        return null;

                    return new CustomAttributeArgument(typeref, boxedValue);

                case MetadataType.Char:
                case MetadataType.Byte:
                case MetadataType.SByte:
                case MetadataType.Int16:
                case MetadataType.UInt16:
                case MetadataType.Int32:
                case MetadataType.UInt32:
                case MetadataType.UInt64:
                case MetadataType.Int64:
                case MetadataType.String:
                    return new CustomAttributeArgument(typeref, ConvertStringValue(svalue, typeref));

                case MetadataType.ValueType:
                    var enumType = _context.Resolve(typeref);
                    if (enumType?.IsEnum != true)
                        goto default;

                    var enumField = enumType.Fields.Where(f => f.IsStatic && f.Name == svalue).FirstOrDefault();
                    object evalue = enumField?.Constant ?? svalue;

                    typeref = enumType.GetEnumUnderlyingType();
                    return new CustomAttributeArgument(enumType, ConvertStringValue(evalue, typeref));

                case MetadataType.Class:
                    if (!typeref.IsTypeOf(WellKnownType.System_Type))
                        goto default;

                    var diagnosticContext = new DiagnosticContext(new MessageOrigin(provider), diagnosticsEnabled: true, _context);
                    if (!_context.TypeNameResolver.TryResolveTypeName(svalue, diagnosticContext, out TypeReference? type, out _, needsAssemblyName: false))
                    {
                        _context.LogError(GetMessageOriginForPosition(nav), DiagnosticId.CouldNotResolveCustomAttributeTypeValue, svalue);
                        return null;
                    }

                    return new CustomAttributeArgument(typeref, type);
                case MetadataType.Array:
                    if (typeref is ArrayType arrayTypeRef)
                    {
                        var elementType = arrayTypeRef.ElementType;
                        var arrayArgumentIterator = nav.SelectChildren("argument", string.Empty);
                        ArrayBuilder<CustomAttributeArgument> elements = default;
                        foreach (XPathNavigator elementNav in arrayArgumentIterator)
                        {
                            if (ReadCustomAttributeArgument(elementNav, provider) is CustomAttributeArgument arg)
                            {
                                // To match Cecil, elements of a list that are subclasses of the list type must be boxed in the base type
                                // e.g. object[] { 73 } translates to Cecil.CAA { Type: object[] : Value: CAA{ Type: object, Value: CAA{ Type: int, Value: 73} } }
                                if (arg.Type == elementType)
                                {
                                    elements.Add(arg);
                                }
                                // This check allows the xml to be less verbose by allowing subtypes to not be boxed in the Array's element type
                                // e.g. here string doesn't need to be boxed in an "object" argument
                                // <argument type="System.Object[]">
                                //   <argument type="System.String">hello</argument>
                                // </argument>
                                //
                                else if (arg.Type.IsSubclassOf(elementType.Namespace, elementType.Name, _context))
                                {
                                    elements.Add(new CustomAttributeArgument(elementType, arg));
                                }
                                else
                                {
                                    _context.LogError(GetMessageOriginForPosition(nav), DiagnosticId.UnexpectedAttributeArgumentType, typeref.GetDisplayName());
                                }
                            }
                            else
                            {
                                return null;
                            }
                        }
                        return new CustomAttributeArgument(arrayTypeRef, elements.ToArray());
                    }
                    goto default;
                default:
                    // No support for null, consider adding - dotnet/linker/issues/1957
                    _context.LogError(GetMessageOriginForPosition(nav), DiagnosticId.UnexpectedAttributeArgumentType, typeref.GetDisplayName());
                    return null;
            }

            TypeReference? ResolveArgumentType(XPathNavigator nav, ICustomAttributeProvider provider)
            {
                string typeName = GetAttribute(nav, "type");
                if (string.IsNullOrEmpty(typeName))
                    typeName = "System.String";

                var diagnosticContext = new DiagnosticContext(new MessageOrigin(provider), diagnosticsEnabled: true, _context);
                if (!_context.TypeNameResolver.TryResolveTypeName(typeName, diagnosticContext, out TypeReference? typeref, out _, needsAssemblyName: false))
                {
                    _context.LogError(GetMessageOriginForPosition(nav), DiagnosticId.TypeUsedWithAttributeValueCouldNotBeFound, typeName, nav.Value);
                    return null;
                }

                return typeref;
            }
        }

        object? ConvertStringValue(object value, TypeReference targetType)
        {
            TypeCode typeCode;
            switch (targetType.MetadataType)
            {
                case MetadataType.String:
                    typeCode = TypeCode.String;
                    break;
                case MetadataType.Char:
                    typeCode = TypeCode.Char;
                    break;
                case MetadataType.Byte:
                    typeCode = TypeCode.Byte;
                    break;
                case MetadataType.SByte:
                    typeCode = TypeCode.SByte;
                    break;
                case MetadataType.Int16:
                    typeCode = TypeCode.Int16;
                    break;
                case MetadataType.UInt16:
                    typeCode = TypeCode.UInt16;
                    break;
                case MetadataType.Int32:
                    typeCode = TypeCode.Int32;
                    break;
                case MetadataType.UInt32:
                    typeCode = TypeCode.UInt32;
                    break;
                case MetadataType.UInt64:
                    typeCode = TypeCode.UInt64;
                    break;
                case MetadataType.Int64:
                    typeCode = TypeCode.Int64;
                    break;
                case MetadataType.Boolean:
                    typeCode = TypeCode.Boolean;
                    break;
                case MetadataType.Single:
                    typeCode = TypeCode.Single;
                    break;
                case MetadataType.Double:
                    typeCode = TypeCode.Double;
                    break;
                default:
                    throw new NotSupportedException(targetType.ToString());
            }

            try
            {
                return Convert.ChangeType(value, typeCode);
            }
            catch
            {
                _context.LogError(null, DiagnosticId.CannotConverValueToType, value.ToString() ?? "", targetType.GetDisplayName());
                return null;
            }
        }

        bool GetAttributeType(XPathNavigator nav, string attributeFullName, [NotNullWhen(true)] out TypeDefinition? attributeType)
        {
            string assemblyName = GetAttribute(nav, "assembly");
            if (string.IsNullOrEmpty(assemblyName))
            {
                attributeType = _context.GetType(attributeFullName);
            }
            else
            {
                AssemblyDefinition? assembly;
                try
                {
                    assembly = _context.TryResolve(AssemblyNameReference.Parse(assemblyName));
                    if (assembly == null)
                    {
                        LogWarning(nav, DiagnosticId.XmlCouldNotResolveAssemblyForAttribute, assemblyName, attributeFullName);

                        attributeType = default;
                        return false;
                    }
                }
                catch (Exception)
                {
                    LogWarning(nav, DiagnosticId.XmlCouldNotResolveAssemblyForAttribute, assemblyName, attributeFullName);
                    attributeType = default;
                    return false;
                }

                attributeType = _context.TryResolve(assembly, attributeFullName);
            }

            if (attributeType == null)
            {
                LogWarning(nav, DiagnosticId.XmlAttributeTypeCouldNotBeFound, attributeFullName);
                return false;
            }

            return true;
        }

        protected override AllowedAssemblies AllowedAssemblySelector
        {
            get
            {
                if (_resource?.Assembly == null)
                    return AllowedAssemblies.AllAssemblies;

                // Corelib XML may contain assembly wildcard to support compiler-injected attribute types
                if (_resource?.Assembly.Name.Name == PlatformAssemblies.CoreLib)
                    return AllowedAssemblies.AllAssemblies;

                return AllowedAssemblies.ContainingAssembly;
            }
        }

        protected override void ProcessAssembly(AssemblyDefinition assembly, XPathNavigator nav, bool warnOnUnresolvedTypes)
        {
            PopulateAttributeInfo(assembly, nav);
            ProcessTypes(assembly, nav, warnOnUnresolvedTypes);
        }

        protected override void ProcessType(TypeDefinition type, XPathNavigator nav)
        {
            Debug.Assert(ShouldProcessElement(nav));

            PopulateAttributeInfo(type, nav);
            ProcessTypeChildren(type, nav);

            if (!type.HasNestedTypes)
                return;

            foreach (XPathNavigator nestedTypeNav in nav.SelectChildren("type", string.Empty))
            {
                foreach (TypeDefinition nested in type.NestedTypes)
                {
                    if (nested.Name == GetAttribute(nestedTypeNav, "name") && ShouldProcessElement(nestedTypeNav))
                        ProcessType(nested, nestedTypeNav);
                }
            }
        }

        protected override void ProcessField(TypeDefinition type, FieldDefinition field, XPathNavigator nav)
        {
            PopulateAttributeInfo(field, nav);
        }

        protected override void ProcessMethod(TypeDefinition type, MethodDefinition method, XPathNavigator nav, object? customData)
        {
            PopulateAttributeInfo(method, nav);
            ProcessReturnParameters(method, nav);
            ProcessParameters(method, nav);
        }

        void ProcessParameters(MethodDefinition method, XPathNavigator nav)
        {
            Debug.Assert(_attributeInfo != null);
            foreach (XPathNavigator parameterNav in nav.SelectChildren("parameter", string.Empty))
            {
                var (attributes, origins) = ProcessAttributes(parameterNav, method);
                if (attributes != null && origins != null)
                {
                    string paramName = GetAttribute(parameterNav, "name");
#pragma warning disable RS0030 // MethodReference.Parameters is banned. It's easiest to leave existing code as is
                    foreach (ParameterDefinition parameter in method.Parameters)
                    {
                        if (paramName == parameter.Name)
                        {
                            if (parameter.HasCustomAttributes || _attributeInfo.CustomAttributes.ContainsKey(parameter))
                                LogWarning(parameterNav, DiagnosticId.XmlMoreThanOneValueForParameterOfMethod, paramName, method.GetDisplayName());
                            _attributeInfo.AddCustomAttributes(parameter, attributes, origins);
                            break;
                        }
                    }
#pragma warning restore RS0030
                }
            }
        }

        void ProcessReturnParameters(MethodDefinition method, XPathNavigator nav)
        {
            Debug.Assert(_attributeInfo != null);
            bool firstAppearance = true;
            foreach (XPathNavigator returnNav in nav.SelectChildren("return", string.Empty))
            {
                if (firstAppearance)
                {
                    firstAppearance = false;
                    var (attributes, origins) = ProcessAttributes(returnNav, method);
                    if (attributes != null && origins != null)
                    {
                        _attributeInfo.AddCustomAttributes(method.MethodReturnType, attributes, origins);
                    }
                }
                else
                {
                    LogWarning(returnNav, DiagnosticId.XmlMoreThanOneReturnElementForMethod, method.GetDisplayName());
                }
            }
        }

        protected override MethodDefinition? GetMethod(TypeDefinition type, string signature)
        {
            if (type.HasMethods)
                foreach (MethodDefinition method in type.Methods)
                    if (signature.Replace(" ", "") == GetMethodSignature(method) || signature.Replace(" ", "") == GetMethodSignature(method, true))
                        return method;

            return null;
        }

#pragma warning disable RS0030 // MethdReference.Parameters is banned. It's easiest to leave existing code as is.
        static string GetMethodSignature(MethodDefinition method, bool includeReturnType = false)
        {
            StringBuilder sb = new StringBuilder();
            if (includeReturnType)
            {
                sb.Append(method.ReturnType.FullName);
            }
            sb.Append(method.Name);
            if (method.HasGenericParameters)
            {
                sb.Append('<');
                for (int i = 0; i < method.GenericParameters.Count; i++)
                {
                    if (i > 0)
                        sb.Append(',');

                    sb.Append(method.GenericParameters[i].Name);
                }
                sb.Append('>');
            }
            sb.Append('(');
            if (method.HasMetadataParameters())
            {
                for (int i = 0; i < method.Parameters.Count; i++)
                {
                    if (i > 0)
                        sb.Append(',');

                    sb.Append(method.Parameters[i].ParameterType.FullName);
                }
            }
            sb.Append(')');
            return sb.ToString();
        }
#pragma warning restore RS0030

        protected override void ProcessProperty(TypeDefinition type, PropertyDefinition property, XPathNavigator nav, object? customData, bool fromSignature)
        {
            PopulateAttributeInfo(property, nav);
        }

        protected override void ProcessEvent(TypeDefinition type, EventDefinition @event, XPathNavigator nav, object? customData)
        {
            PopulateAttributeInfo(@event, nav);
        }

        void PopulateAttributeInfo(ICustomAttributeProvider provider, XPathNavigator nav)
        {
            Debug.Assert(_attributeInfo != null);
            var (attributes, origins) = ProcessAttributes(nav, provider);
            if (attributes != null && origins != null)
                _attributeInfo.AddCustomAttributes(provider, attributes, origins);
        }
    }
}
